/*
 * TiledMapBrowser.java
 *
 * Created on 29 avril 2007, 21:54
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package courselog.maputils;

import courselog.AbstractImageDisplay;
import courselog.CourseLogAppForm;
import courselog.MainApplication;
import courselog.gpsutils.Converters;
import courselog.gpsutils.PositionTracer;
import courselog.gpsutils.TrackPoint;
import courselog.gpsutils.XPositionDisplayer;
import courselog.gpsutils.GeoDirection;
import courselog.gpsutils.GeoPoint;
import courselog.gpsutils.SerialGPSConnection;
import courselog.gpsutils.TrackRecord;
import ewe.fx.Color;
import ewe.fx.Dimension;
import ewe.fx.Graphics;
import ewe.fx.Image;
import ewe.fx.Pen;
import ewe.fx.Point;
import ewe.fx.Rect;
import ewe.io.File;
import ewe.sys.Handle;
import ewe.sys.HandleStoppedException;
import ewe.sys.MemoryStatus;
import ewe.sys.mThread;
import ewe.ui.Event;
import ewe.ui.EventListener;
import ewe.ui.IKeys;
import ewe.ui.KeyEvent;
import ewe.ui.Menu;
import ewe.ui.PenEvent;
import ewe.ui.mLabel;


/**
 *
 * @author rigal
 */
public class TiledMapBrowser extends AbstractImageDisplay implements XPositionDisplayer {
    
    /** Update option indicating that Waypoints are unchanged */
    public static final int OPT_WPUNCHANGED=1;
    
    /** Update option indicating that Track are unchanged */
    public static final int OPT_TRUNCHANGED=2;
    
    /** Pen for visited waypoints */
    public static final Pen PENVISITEDWP=new Pen(new Color(60,180,220),Pen.SOLID,1);
    
    /** Pen for non visited waypoints */
    public static final Pen PENNONVISITEDWP=new Pen(new Color(60,180,220),Pen.SOLID,2);
    
    /** Pen for Track */
    public static final Pen PENTRACK=new Pen(new Color(220,40,220),Pen.SOLID,2);
//    public static final Pen PENTRACK=new Pen(new Color(0,0,0),Pen.SOLID,2);
    
    /** Pen for Messages */
//    public static final Pen PENOSD=new Pen(new Color(210,120,130),Pen.SOLID,1);
    
    /** Font for Messages */
//    public static Font fontOSD=null;
    
    /**
     * Square of distance under which a waypoint is considered visited.
     * Distance is in pixels so it depends on the current zoom setting.
     * Equals 8*8.
     */
    public static final int VISITDISTANCESQUARE=8*8;
    
    /** Memory threshold to start freeing maps which are unused == 80 (%) */
    public final int MEMORYTHRESHOLD=80;
    
    /** X values of polygon drawing a position symbol. */
    public static final int[] tXPt=new int[]{4,-4,-4,0,4,4,0,-4};
    
    /** Y values of polygon drawing a position symbol. */
    public static final int[] tYPt=new int[]{0,0,6,0,6,0,-6,0};
    
    /** X values of polygon drawing the direction symbol */
    protected int tXDir[]= new int[3];
    
    /** Y values of polygon drawing the direction symbol */
    protected int tYDir[]= new int[3];
    
    /** Temp X values of polygon drawing a position symbol. */
    public int[] tX=new int[tXPt.length];
    
    /** Temp Y values of polygon drawing a position symbol. */
    public int[] tY=new int[tXPt.length];
    
    /**
     * myState bit if the browser follows the current position.
     * If not set, the browser moves are free and waypoints entering are
     * enabled.
     */
    public static final int STATE_FOLLOW=1;
    
    /** myState bit if track is displayed. */
    public static final int STATE_DISPLAYTR=2;
    
    /** myState bit if waypoints are displayed. */
    public static final int STATE_DISPLAYWP=4;
    
    /** myState bit if active. */
    public static final int STATE_ACTIVE=8;
    
    /** myState bit if information is displayed. */
    public static final int STATE_INFO=16;
    
    /** myState bit if waypoints are locked. */
    public static final int STATE_LOCKWP=32;
    
    /** Distance to select a point ==4 */
    public static final int MAXDIST=12;
    
    /** My CalibratedMap */
    public TiledMap myMap=null;
    
    /** Pool of images to swap for display */
    public Image myDispImages[]=new Image[2];
    
    /** Pool of graphics associated to the images */
    public Graphics myDispGraphics[]=new Graphics[2];
    
    /** Handle to allow displayed image update. */
    protected Handle myUpdateHandle=new Handle();
    
    /** Index of the active image [0,1]. */
    public int myIdxDisp=0;
    
    /** Menu to display for pen held on a waypoint. */
    protected Menu myMenPenHeldWP=null;
    
    /** Menu to display for pen held not on a waypoint. */
    protected Menu myMenPenHeldNoWP=null;
    
    /** Memory of last waypoint which had a pen held on it. */
    protected int myIdxLastPenHeld=-1;
    
    /** Memory of last point which had a pen held on it. */
    protected Point myLastPenHeldPoint=new Point(0,0);
    
    /**
     * Status of the browser.
     * Should be a combination of the STATE_XXX bits.
     */
    public int myState=(STATE_FOLLOW|STATE_DISPLAYWP|STATE_DISPLAYTR|STATE_INFO);
    
    /**
     * Event listener handling swap state action.
     */
    public EventListener mySwapHandler=null;
    
    /**
     * Event to send to the listener.
     */
    public Event mySwapEvent=null;
    
    /** Pen for normal drawing central area circle.
     */
    public static final Pen PENPOSITION=new Pen(new Color(255,90,10),Pen.SOLID,2);
    
    /** Pen for normal drawing of points.
     */
    public static final Pen NORMALPEN=new Pen(MainApplication.NORMALCOLOR,Pen.DASH,2);
    
    /** Pen for drawing Position when lost. */
    public static final Pen LOSTPEN=new Pen(MainApplication.LOSTCOLOR,Pen.DASH,2);
    
    /** Pen for drawing position when disconnected from GPS. */
    public static final Pen DISCONNECTPEN=new Pen(MainApplication.BADCOLOR,Pen.DASH,2);
    
    /** Current drawing pen. */
    public Pen currentPen=NORMALPEN;
    
    /** Mobile position geo point object. */
    public TrackPoint myCurrentPosition=null;
    
    /** The GPS current status. */
    public int myGPSStatus=SerialGPSConnection.STATUS_NOK;
    
    /** temporary point object. */
    public Point tmpPt=new Point();
    
    /** temporary Rect object. */
    public Rect tmpRe=new Rect();
    
    /** temporary Dimension object. */
    public Dimension tmpDim=new Dimension();
    
    /** Symbol point to display when point is dragged.
     * When position is negative, no point displayed.
     */
    public Point ptDrag=new Point(-1,-1);
    
    /** temporary Direction object. */
    public GeoDirection tmpGD=new GeoDirection();
    
    /** Last Scaling factor */
    protected double myLastSc;
    
    /** Last selected point */
    protected double myLastSelected=-1;
    
    /** Last Center circle radius */
    protected int myCenterRadius;
    
    /** Current track of the receiver */
    protected MapTrack myTrCurrent;
    
    /** Flag indicated if waypoints need to be saved. */
    public boolean myIsWaypointsChanged;
    
    /** Waypoints track */
    public MapTrack myTrWaypoints;
    
    /** Label displaying the point status. */
    public mLabel myLPoint=null;
    
    /** Label displaying the track status. */
    public mLabel myLTrack=null;
    
    // CONSTRUCTORS
    
    /**
     * Creates a new instance of CalibratedMapBrowser from an image file name.
     */
    public TiledMapBrowser(String mapDir) {
        modify(WantHoldDown,0);
        setTiledMapFrom(mapDir);
        myTrWaypoints=new MapTrack(PENVISITEDWP,PENNONVISITEDWP);
        myIsWaypointsChanged=false;
    }
    
    // ACCESSORS
    
    public boolean isActive() {
        return (myState&STATE_ACTIVE)!=0;
    }
    
    public void setActive(boolean active) {
        if(active) {
            myState |= STATE_ACTIVE;
        }else{
            myState &= (~STATE_ACTIVE);
        }
    }
    
    /**
     * Getter for myTrCurrent.
     */
    public MapTrack getMyTrCurrent() {
        return myTrCurrent;
    }
    
    /**
     * Setter for display labels.
     * Set the display objects to null if you want to deactivate displays.
     */
    public void setDisplays(mLabel lDispPoint, mLabel lDispTrack) {
        myLPoint=lDispPoint;
        myLTrack=lDispTrack;
    }
    
    /**
     * Getter for next waypoint.
     */
    public int getNextWaypoint() {
        return myTrWaypoints.myIndexNext;
    }
    
    /**
     * Setter for next waypoint.
     */
    public void setNextWaypoint(int next) {
        myTrWaypoints.setNextTo(next);
    }
    
    /**
     * Getter for selected waypoint.
     */
    public int getSelectedWaypoint() {
        return myTrWaypoints.myIndexSelected;
    }
    
    /**
     * Setter for selected waypoint.
     */
    public void setSelectedWaypoint(int next) {
        myTrWaypoints.setSelectedIndex(next);
    }
    
    /**
     * Setter for pen held menus.
     * Set the menus object to null if you want to deactive menu.
     */
    public void setPenHeldMenus(Menu onWaypoint, Menu onNoWaypoint) {
        myMenPenHeldWP=onWaypoint;
        myMenPenHeldNoWP=onNoWaypoint;
    }
    
    /**
     * Setter for swap action shortcut control.
     * Set the control that should be activated when the shortcut for swaping
     *  the state is activated.
     */
    public void setSwapStateHandler(EventListener handler, Event ev) {
        mySwapHandler=handler;
        mySwapEvent=ev;
    }
    
    /**
     * Set the waypoints locked bit.
     * @param st true if yes, false otherwise.
     */
    public void setWPLocked(boolean st) {
        if(st) {
            myState |= STATE_LOCKWP;
            if(myTrWaypoints.getSize()>=0) {
                if(myTrWaypoints.myIndexNext<0) {
                    myTrWaypoints.myIndexNext=0;
                }
                if(myTrWaypoints.myIndexSelected<0) {
                    myTrWaypoints.myIndexSelected=0;
                }
            }else{
                myTrWaypoints.myIndexNext=-1;
                myTrWaypoints.myIndexSelected=-1;
            }
        }else{
            myState &= ~STATE_LOCKWP;
        }
    }
    // CLASS METHODS
    
    /**
     * Set tiled map from a directory name where the calibrated maps are.
     */
    public void setTiledMapFrom(String imageDirName) {
        if(myMap!=null) {
            reAllocateImages(0,0);
//            myMap.free();
        }
        myMap = new TiledMap(imageDirName);
        myLastSc=myMap.dScPixelOnMeters;
//        myCenterRadius=0;
    }
    
    /**
     * Clears the pool of images used by the browser, re-initialises with new
     * sizes, initialises the image index, the graphics objects.
     * If called with either width or height at 0, the new image objects are
     * not allocated.
     * @param width width of the images to create.
     * @param heigth height of the images to create.
     */
    public void reAllocateImages(int width, int height) {
        if(myDispImages[0]!=null) {
            myDispImages[0].free();
            myDispImages[0]=null;
        }
        if(myDispImages[1]!=null) {
            myDispImages[1].free();
            myDispImages[1]=null;
        }
        if((width>0)&&(height>0)) {
            myDispImages[0]= new Image(width,height);
            myDispGraphics[0]= new Graphics(myDispImages[0]);
            myDispImages[1]= new Image(width,height);
            myDispGraphics[1]= new Graphics(myDispImages[1]);
        }
        dispImage=myDispImages[0];
        myIdxDisp=0;
    }
    
    /**
     * Reset the track.
     */
    public void resetWaypoints() {
        myTrWaypoints.removeAll();
        myIsWaypointsChanged=false;
        setWPLocked(false);
        updateDisplayedImage(null,OPT_TRUNCHANGED);
    }
    
    /**
     * Rescale Tiled map to one at the current center.
     */
    public void reScaleOne() {
        myMap.dScPixelOnMeters=myMap.findCenterCloserScale();
        updateDisplayedImage(null,0);
    }
    
    /**
     * Rescale Tiled map to the smaller scale at the current center.
     */
    public void reScaleMin() {
        myMap.dScPixelOnMeters=myMap.findCenterMinScale();
        updateDisplayedImage(null,0);
    }
    
    /**
     * Rescale Tiled map to the larger scale at the current center.
     */
    public void reScaleMax() {
        myMap.dScPixelOnMeters=myMap.findCenterMaxScale();
        updateDisplayedImage(null,0);
    }
    
    /**
     * Set last selected waypoint to be the next to visit.
     */
    public void visitPenHeldWaypoint() {
        myTrWaypoints.setNextTo(myIdxLastPenHeld);
        updateDisplayedImage(null,0);
    }
    
    /**
     * Re-center on last selected point.
     */
    public void reCenterOnPenHeldPoint() {
        iOX=width/2-myLastPenHeldPoint.x;
        iOY=height/2-myLastPenHeldPoint.y;
        updateDisplayedImage(null,0);
    }
    
    /**
     * Re-center Tiled map.
     */
    public void reCenter(GeoPoint newCenter) {
//        setTiledMapFrom(myApp.getMyMapDir());
        myMap.setMapCenter(myMap.gpCenter);
        Rect rD=this.getDim(new Rect());
        myMap.gpCenter.iX=rD.width/2;
        myMap.gpCenter.iY=rD.height/2;
        myLastSc=0.0d; // Easy way to ask for redraw
        updateDisplayedImage(null,0);
    }
    
//    public void removeSelectedWaypoint() {
//        removeWaypointAt(myTrWaypoints.myIndexSelected);
//    }
//
    public void invertWaypoints() {
        myTrWaypoints.reverse();
        myIsWaypointsChanged=true;
        updateDisplayedImage(null,OPT_TRUNCHANGED);
    }
    
    public void removePenHeldWaypoint() {
        removeWaypointAt(myIdxLastPenHeld);
    }
    
    public void removeWaypointAt(int idxR) {
        myTrWaypoints.removeAt(idxR);
        int sz=myTrWaypoints.getSize();
        if(myTrWaypoints.myIndexSelected>=sz) myTrWaypoints.myIndexSelected=sz-1;
        myIsWaypointsChanged=true;
        updateDisplayedImage(null,OPT_TRUNCHANGED);
    }
    
    /**
     * Draw a point position.
     */
    public void drawRefPointSymbolAt(Graphics g, int x, int y) {
        drawPositionSymbolAt(g,x,y,0.0d);
    }
    
    /**
     * Draw a direction symbol on the central circle.
     */
    public void drawDirectionSymbolFor(Graphics gD, double dir_deg) {
        double sinH=dir_deg*Math.PI/180.0d;
        double cosH=Math.cos(sinH);
        sinH=Math.sin(sinH);
        int x=(int) (this.width/2+myCenterRadius*sinH);
        int y=(int) (this.height/2-myCenterRadius*cosH);
        
        tXDir[0]=(int)(12*sinH);
        tYDir[0]=(int)(-12*cosH);
        tXDir[1]=-(tXDir[0]+tYDir[0])/2;
        tYDir[1]=(tXDir[0]-tYDir[0])/2;
        tXDir[2]=-tYDir[1];
        tYDir[2]=tXDir[1];
        
        gD.translate(x,y);
        gD.fillPolygon(tXDir,tYDir,tXDir.length);
        gD.translate(-x,-y);
    }
    
    /**
     * Draw a point position.
     */
    public void drawPositionSymbolAt(Graphics gD, int x, int y, double dir_deg) {
//        gD.setPen(currentPen);
        gD.translate(x,y);
        if(dir_deg==0) {
            gD.drawPolygon(tXPt,tYPt,tXPt.length);
        }else{
            double sin=dir_deg*Math.PI/180.0d;
            double cos=Math.cos(sin);
            sin=Math.sin(sin);
            for(int idx=tXPt.length-1; idx>=0; idx--) {
                tX[idx]=(int)(tXPt[idx]*cos-tYPt[idx]*sin);
                tY[idx]=(int)(tXPt[idx]*sin+tYPt[idx]*cos);
            }
            gD.drawPolygon(tX,tY,tX.length);
        }
        gD.translate(-x,-y);
    }
    
    /**
     * Draw the on-screen message selected by the user.
     */
    public void drawOnScreenInformation(Graphics g) {
//        g.setPen(PENOSD);
//        g.setFont(fontOSD);
        String msgP=null;
        String msgT=null;
        TrackPoint sPt=null;
        TrackPoint origPt=null;
        
        if(myCurrentPosition==null) {
            if((myState&STATE_FOLLOW)==0) {
                sPt=myTrWaypoints.getSelected();
                if((myTrWaypoints.myIndexSelected>0)&&(myTrWaypoints.getSize()>1)) {
                    origPt=myTrWaypoints.getPointAt(
                            myTrWaypoints.myIndexSelected-1);
                }
            }else{
                sPt=myTrWaypoints.getNext();
                if((myTrWaypoints.myIndexNext>0)&&(myTrWaypoints.getSize()>1)) {
                    origPt=myTrWaypoints.getPointAt(
                            myTrWaypoints.myIndexNext-1);
                }
            }
        }else{
            origPt=myCurrentPosition;
            if((myState&STATE_FOLLOW)==0) {
                sPt=myTrWaypoints.getSelected();
            }else{
                sPt=myTrWaypoints.getNext();
            }
        }
        if(sPt!=null) {
            if((origPt==null)||((myState&STATE_LOCKWP)==0)) {
                msgP="Lat: "+Converters.ddmmss(sPt.lat,Converters.DDMMSS_LAT)+
                        " Lon: "+Converters.ddmmss(sPt.lon,Converters.DDMMSS_LON);
                msgT="DT:"+Converters.formatDistance(myTrWaypoints.getTotalLength());
            }else{
                double hdg=origPt.headingTo(sPt);
                double dist=sPt.distance(origPt);
                double distTotal=myTrWaypoints.getTotalLength()-
                        sPt.lengthHere+dist;
                double duration=origPt.avgSpeed/3.6d; // intermediate variable
                if(duration==0.0d) {
                    duration=origPt.speed/3.6d;
                }
                double totalDuration=0.0d;
                if(duration>0.0d) {
                    totalDuration=distTotal/duration;
                    duration=dist/duration;
                }else{
                    totalDuration=0.0d;
                    duration=0.0d;
                }
                msgP="D:"+
                        Converters.formatDistance(dist)+
                        " Hdg:"+
                        Converters.formatHeading(hdg)+
                        " T:"+
                        Converters.formatDuration(duration);
                
                msgT="DT:"+Converters.formatDistance(distTotal)+
                        " TT:"+Converters.formatDuration(totalDuration);
            }
            int nextY=0;
            if((msgP!=null)&&(myLPoint!=null)) {
                myLPoint.setText(msgP);
//                g.getSize(g.getFontMetrics(fontOSD),msg,tmpDim);
//                nextY=tmpDim.height+1;
//                g.setColor(bgC);
//                g.fillRect(0,0,this.width,tmpDim.height);
//                g.setColor(PENOSD.color);
//                g.drawText(msg,0,0);
            }
            if((msgT!=null)&&(myLTrack!=null)) {
                myLTrack.setText(msgT);
            }
        }
    }
    
    /**
     * Draw the followed position of the navigating mobile.
     */
    public void drawMyPosition(Graphics gD) {
        gD.setPen(PENPOSITION);
        gD.setDrawOp(Graphics.DRAW_OVER);
        if((myGPSStatus==SerialGPSConnection.STATUS_OK)&&
                (myCurrentPosition!=null)) {
            tmpPt=myMap.getMapPointFromPosition(myCurrentPosition,tmpGD,tmpPt);
            if((tmpPt.x>=0)&&(tmpPt.x<width)&&(tmpPt.y>=0)&&(tmpPt.y<height)) {
                drawPositionSymbolAt(gD,tmpPt.x,tmpPt.y,myCurrentPosition.avgHeading);
            }
            GeoPoint sPt=myTrWaypoints.getNext();
            if(sPt!=null) {
                double hdg=myCurrentPosition.headingTo(sPt)-
                        myCurrentPosition.heading;
                drawDirectionSymbolFor(gD,hdg);
            }
            gD.setPen(NORMALPEN);
        }else if(myGPSStatus==SerialGPSConnection.STATUS_GPSLOST) {
            drawPositionSymbolAt(gD,width/2,height/2,0.0d);
            gD.setPen(LOSTPEN);
        }else if(myGPSStatus==SerialGPSConnection.STATUS_NOK) {
            drawPositionSymbolAt(gD,width/2,height/2,0.0d);
            gD.setPen(DISCONNECTPEN);
        }
        if((myState&STATE_FOLLOW)!=0) {
            gD.drawEllipse(width/2-myCenterRadius,height/2-myCenterRadius,
                    myCenterRadius*2,myCenterRadius*2);
            //TODO draw a symbol to direction
        }
    }
    
    /**
     * save the waypoints.
     */
    public void saveWaypoints(File fWp) {
        if(myTrWaypoints==null) return;
        myTrWaypoints.writeToFile(fWp);
        myIsWaypointsChanged=false;
    }
    
    /**
     * Save the track.
     */
    public void saveTrack(File fWp) {
        if(myTrCurrent==null) return;
        myTrCurrent.writeToFile(fWp);
    }
    
    /**
     * Load waypoints from a log file.
     */
    public void loadWaypoints(File fWp) {
        myTrWaypoints.loadFromFile(fWp);
        myTrWaypoints.setNextTo(0);
//        myState |= STATE_DISPLAYWP;
        updateDisplayedImage(null,OPT_TRUNCHANGED);
        myIsWaypointsChanged=false;
    }
    
    /**
     * Update the current position and drawing mode according to tracer status.
     */
    public boolean updateTracerStatus(PositionTracer pTr) {
        myGPSStatus=pTr.getGPSStatus();
        boolean posUpdated=(myGPSStatus==SerialGPSConnection.STATUS_OK);
        if(posUpdated) {
            myCurrentPosition=pTr.myCurrent;
            tmpPt=myMap.getMapPointFromPosition(myCurrentPosition,
                    tmpGD,tmpPt);
            int dx=width/2-tmpPt.x;
            int dy= height/2-tmpPt.y;
            double d=dx*dx+dy*dy;
            if(((myState&STATE_FOLLOW)!=0)&&(d>(myCenterRadius*myCenterRadius))) {
//                    myMap.moveCenterLimited(tmpPt,getDim(tmpRe));
                iOX=dx;
                iOY=dy;
                return true;
            }
            GeoPoint nPt=myTrWaypoints.getNext();
            if(nPt!=null) {
                dx= -tmpPt.x;
                dy= -tmpPt.y;
                tmpPt=myMap.getMapPointFromPosition(nPt,tmpGD,tmpPt);
                dx += tmpPt.x;
                dy += tmpPt.y;
                d=dx*dx+dy*dy;
                if(d<VISITDISTANCESQUARE) {
//                    CourseLogAppForm.dbgL.addToLog("TiledMapBrowser.updateTracerStatus: " +
//                            nPt+"visited");
                    myTrWaypoints.visitNext();
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Update the trace on the map according to the new track.
     */
    public void updateTrackData(PositionTracer pTr) {
        // Nothing special to do with the track, the TrackRecord is linked
        // to the PositionTracer and next update will draw it.
    }
    
    // INTERFACE METHODS
    
    /**
     * Called when the PositionTracer changes.
     * @see courselog.gpsutils.XPositionDisplayer
     * @param opt option indicating if status and/or trakc changed
     * @param pTr Tracer which has been updated.
     */
    public void listenerUpdated(int opt, PositionTracer pTr) {
        boolean needUpdate=((opt&PositionTracer.OPT_TRACECHANGED)!=0);
        if(needUpdate) {
            updateTrackData(pTr);
        }
        if((opt&PositionTracer.OPT_STATUSCHANGED)!=0) {
            boolean isImageModified=updateTracerStatus(pTr); //Need to do it in 2 lines, otherwise
            needUpdate = (needUpdate||isImageModified); // optimised and not called!
        }
        if(needUpdate) {
            updateDisplayedImage(null,0);
        }else{
            repaint();
        }
    }
    
    /**
     * Setter for current track.
     */
    public void setCurrentTrack(TrackRecord tr) {
        this.myTrCurrent=new MapTrack(PENTRACK,PENTRACK,tr);
        if((myState&STATE_DISPLAYTR)==1) {
            updateDisplayedImage(null,OPT_WPUNCHANGED);
        }
    }
    
    // OVERIDDEN METHODS
    
    /**
     * Trap key events to allow keyboard shortcuts.
     */
    public void onKeyEvent(KeyEvent ev) {
        if(ev.type==KeyEvent.KEY_PRESS) {
            if((ev.key=='n')||(ev.key=='N')||(ev.key=='3')) {
                myTrWaypoints.selectNext(1);
                updateDisplayedImage(null,OPT_TRUNCHANGED);
            } else if((ev.key=='b')||(ev.key=='B')||(ev.key=='1')) {
                myTrWaypoints.selectNext(-1);
                updateDisplayedImage(null,OPT_TRUNCHANGED);
            }else if((ev.key==IKeys.BACKSPACE)||(ev.key=='2')) {
                myTrWaypoints.setSelectedIndex(-1);
                updateDisplayedImage(null,OPT_TRUNCHANGED);
            }else if((ev.key==IKeys.TAB)||(ev.key=='s')||(ev.key=='S')||(ev.key=='0')) {
                if(myTrWaypoints.myIndexSelected>=0) {
                    tmpPt=myTrWaypoints.getMapPointFromPosition(
                            myTrWaypoints.myIndexSelected,tmpGD,tmpPt,myMap);
                }else{
                    tmpPt.set(this.width/2,this.height/2);
                }
                actionOnPenHeld(tmpPt);
            }else if((ev.key=='w')||(ev.key=='W')||(ev.key=='5')) {
                tmpPt.set(this.width/2,this.height/2);
                actionOnPenClick(tmpPt);
            }
        }
    }
    
    /**
     * On pen drag, if the user tries to drag a waypoint, select it, move it.
     * Otherwise, do as normally on an AbstractImageDisplay.
     */
    public void penDrag(PenEvent pEv) {
        int idxD=myTrWaypoints.indexOfClosestPoint(tmpPt.set(pEv.x,pEv.y),MAXDIST,myMap);
        if(((idxD<0)&&(ptDrag.x<0))||((myState&STATE_LOCKWP)!=0)) {
            super.penDrag(pEv);
        }else{
            ptDrag.set(pEv.x,pEv.y);
            if(idxD>=0) myTrWaypoints.myIndexSelected=idxD;
            repaint();
        }
    }
    
    /**
     * On pen release, if a waypoint was dragged, commit the move and
     * update the image.
     */
    public void penReleased(Point p) {
        if((ptDrag.x>0)&&((myState&STATE_LOCKWP)==0)) {
            // a waypoint was moved
            myTrWaypoints.movePointAt(myTrWaypoints.myIndexSelected, p, myMap);
            ptDrag.x=-1;
            myIsWaypointsChanged=true;
            updateDisplayedImage(null,OPT_TRUNCHANGED);
            repaint();
        }
        super.penReleased(p);
    }
    
    /**
     * On pen click, if mode is waypoint, place a new point at the end of
     * the track.
     */
    public void actionOnPenClick(Point p) {
        Rect dest=null;
        int idxD=myTrWaypoints.indexOfClosestPoint(p,MAXDIST,myMap);
        if(idxD<0) {
            if((myState&STATE_LOCKWP)!=0) return;
//            CourseLogAppForm.dbgL.addToLog("TiledMapBrowser.actionOnPenClick: idxD="+idxD);
            myTrWaypoints.addLast(p, myMap);
//            myTrWaypoints.myIndexSelected=myTrWaypoints.getSize()-1;
            myIsWaypointsChanged=true;
        }else{
            if(idxD==myTrWaypoints.myIndexSelected) {
                myTrWaypoints.myIndexSelected=-1;
            }else{
                myTrWaypoints.myIndexSelected=idxD;
            }
            tmpPt=myTrWaypoints.getMapPointFromPosition(
                    myTrWaypoints.myIndexSelected,tmpGD,tmpPt,myMap);
            dest=new Rect(tmpPt.x-MAXDIST,tmpPt.y-MAXDIST,MAXDIST*2,MAXDIST*2);
        }
        updateDisplayedImage(dest,OPT_TRUNCHANGED);
    }
    
    /**
     * Pen held on the displayed image, display a menu depending this is on a
     * waypoint or this is elsewere.
     */
    public void actionOnPenHeld(Point p) {
        int idxD=myTrWaypoints.indexOfClosestPoint(p,MAXDIST,myMap);
        if(idxD<0) {
            if(myMenPenHeldNoWP!=null) {
                myLastPenHeldPoint.set(p);
                this.setMenu(myMenPenHeldNoWP);
//                if(this.doMenu(p))
//                    (new MessageBox("No way point","No way point pressed",MessageBox.MBOK)).execute();
            }
        }else{
            if(myMenPenHeldWP!=null) {
                myIdxLastPenHeld=idxD;
                this.setMenu(myMenPenHeldWP);
//                if(this.doMenu(p))
//                    (new MessageBox("No way point","No way point pressed",MessageBox.MBOK)).execute();
            }
        }
        this.tryStartMenu(p);
    }
    
    /**
     * Double press on enter, consider it as a long click on the center 
     * of the control.
     * @Override AbstractImageDisplay.doublePressEnter(KeyEvent)
     */
    public void doublePressEnter(KeyEvent kEv) {
        if((mySwapHandler!=null)&&(mySwapEvent!=null)) {
            mySwapHandler.onEvent(mySwapEvent);
            updateDisplayedImage(null,0);
            repaint();
        }
    }
    
    /**
     * Modify the displayed image to draw reference points.
     */
    public void updateDisplayedImage(Rect r, int opt) {
        Rect rDim;
        Rect r1=null;
        Rect r2=null;
        
        rDim=getDim(tmpRe);
        
        if((rDim.width==0)||(rDim.height==0)) return;
        
        myUpdateHandle.set(Handle.Running);
        boolean redrawall=true;
        if((dispImage==null)||(dispImage.getWidth()!=rDim.width)||
                (dispImage.getHeight()!=rDim.height)) {
            // Not initialised, don't test the vectors
            reAllocateImages(rDim.width,rDim.height);
            myCenterRadius=((rDim.width>rDim.height?rDim.height:rDim.width)*2)/6;
            myLastSc=0.0d; // triggers a ful redraw
            myMap.gpCenter.iX=rDim.width/2;
            myMap.gpCenter.iY=rDim.height/2;
//        } else {
//            if((myLastSc==myMap.dScPixelOnMeters)&&
//                    ((opt^(OPT_TRUNCHANGED|OPT_WPUNCHANGED))!=0)) redrawall=false;
        }
        
        if(redrawall) {
            if((iOX!=0)||(iOY!=0)) {
                myMap.moveCenterLimited(tmpPt.set(iOX,iOY),rDim);
                iOX=0;
                iOY=0;
            }
            r1=rDim;
        }else{
            if((iOX!=0)||(iOY!=0)) {
                // Center has moved so move the center of the tiled map.
                tmpPt=myMap.moveCenterLimited(tmpPt.set(iOX,iOY),rDim);
                // Move the displayed image itself
                myDispGraphics[1-myIdxDisp].drawImage(myDispImages[myIdxDisp],
                        tmpPt.x,tmpPt.y);
                myIdxDisp=1-myIdxDisp;
                dispImage=myDispImages[myIdxDisp];
                if(tmpPt.x>0) {
                    r1=new Rect(0,0,tmpPt.x,rDim.height);
                    if(tmpPt.y>0) {
                        r2=new Rect(tmpPt.x,0,rDim.width-tmpPt.x,tmpPt.y);
                    }
                    if(tmpPt.y<0) {
                        r2=new Rect(tmpPt.x,rDim.height+tmpPt.y,
                                rDim.width-tmpPt.x,-tmpPt.y);
                    }
                }else if(tmpPt.x<0) {
                    r1=new Rect(rDim.width+tmpPt.x,0,-tmpPt.x,rDim.height);
                    if(tmpPt.y>0) {
                        r2=new Rect(0,0,rDim.width+tmpPt.x,tmpPt.y);
                    }
                    if(tmpPt.y<0) {
                        r2=new Rect(0,rDim.height+tmpPt.y,rDim.width+tmpPt.x,-tmpPt.y);
                    }
                } else {
                    // if iOX is 0, iOY should not
                    if(tmpPt.y>0) {
                        r1=new Rect(0,0,rDim.width,tmpPt.y);
                    }
                    if(tmpPt.y<0) {
                        r1=new Rect(0,rDim.height+tmpPt.y,rDim.width,-tmpPt.y);
                    }
                }
                iOX=0;
                iOY=0;
            }
        }
        
        myLastSc=myMap.dScPixelOnMeters;
        
//        CourseLogAppForm.dbgL.addToLog("update "+r1+" mem="+ewe.sys.Vm.getUsedMemory(false)+"\n");
        MemoryStatus ms=ewe.sys.Vm.getSystemMemoryStatus();
        if(ms!=null) {
//            CourseLogAppForm.dbgL.addToLog("Status "+ms.memoryLoad+"% av"+ms.availablePhysical+"\n");
            if(ms.memoryLoad>MEMORYTHRESHOLD) {
//                for(int i=myMap.myNumberOfUsage.length-1; i>=0; i--) {
//                    CourseLogAppForm.dbgL.addToLog("usageB["+i+"]="+myMap.myNumberOfUsage[i]+"\n");
//                }
                myMap.releaseUnusedMaps();
//                for(int i=myMap.myNumberOfUsage.length-1; i>=0; i--) {
//                    CourseLogAppForm.dbgL.addToLog("usageA["+i+"]="+myMap.myNumberOfUsage[i]+"\n");
//                }
            }
        }
        if(r1!=null) {
            myMap.drawIn(myDispGraphics[myIdxDisp],r1,getBackground());
//            if(((myState&STATE_DISPLAYTR)!=0)&&((opt&OPT_TRUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYTR)!=0) {
                myTrCurrent.drawIn(myDispGraphics[myIdxDisp],r1,myMap);
            }
//            if(((myState&STATE_DISPLAYWP)!=0)&&((opt&OPT_WPUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYWP)!=0) {
                myTrWaypoints.drawIn(myDispGraphics[myIdxDisp],r1,myMap);
            }
        }
        
        if(r2!=null) {
            myMap.drawIn(myDispGraphics[myIdxDisp],r2,getBackground());
//            if(((myState&STATE_DISPLAYTR)!=0)&&((opt&OPT_TRUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYTR)!=0) {
                myTrCurrent.drawIn(myDispGraphics[myIdxDisp],r2,myMap);
            }
//            if(((myState&STATE_DISPLAYWP)!=0)&&((opt&OPT_WPUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYWP)!=0) {
                myTrWaypoints.drawIn(myDispGraphics[myIdxDisp],r2,myMap);
            }
        }
        
        if(r!=null) {
            myMap.drawIn(myDispGraphics[myIdxDisp],r,getBackground());
//            if(((myState&STATE_DISPLAYWP)!=0)&&((opt&OPT_WPUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYWP)!=0) {
                myTrWaypoints.drawIn(myDispGraphics[myIdxDisp],r,myMap);
            }
//            if(((myState&STATE_DISPLAYTR)!=0)&&((opt&OPT_TRUNCHANGED)==0)) {
            if((myState&STATE_DISPLAYTR)!=0) {
                myTrCurrent.drawIn(myDispGraphics[myIdxDisp],r,myMap);
            }
        }
        
        repaint();
        myUpdateHandle.succeed(this);
    }
    
    public void rescaleImage(double byFactor) {
        if(myMap==null) return;
        myMap.dScPixelOnMeters *= byFactor;
    }
    
    public void resizeTo(int width, int height) {
        super.resizeTo(width,height);
        updateDisplayedImage(null,0);
    }
    
    public void doPaint(Graphics g, Rect r) {
        // Override to allow waiting for update
        if(mThread.inThread()) {
            try {
                myUpdateHandle.waitOn(Handle.Success|Handle.Stopped);
            } catch (InterruptedException ex) {
                CourseLogAppForm.dbgL.addExceptionToLog(ex);
            } catch (HandleStoppedException ex) {
                CourseLogAppForm.dbgL.addExceptionToLog(ex);
            }
        }
        super.doPaint(g,r);
        drawMyPosition(g);
        if(ptDrag.x>=0) {
            myTrWaypoints.drawHandle(g,ptDrag.x,ptDrag.y);
        }
        drawOnScreenInformation(g);
    }
    
}
